/*
Copyright (c) 2008 Alexander K Syed
Based on banana.py
Copyright (c) 2001-2004 Twisted Matrix Laboratories.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package org.pixelami.twisted.spread.pb
{
	import flash.events.Event;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	
	import org.pixelami.twisted.errors.NotImplementedError;
	import org.pixelami.twisted.spread.errors.BananaError;
	import org.pixelami.twisted.utils.StreamTools;
	
	public class Banana extends Protocol
	{
		static public var _PREFIX_LIMIT:int = 64;
		
		public var currentDialect:String;
		public var knowDialects:Array = ["pb","None"];
		
		private var buffer:ByteArray;
		private var prefixLimit:int;
		private var _smallestLongInt:int;
		private var _largestLongInt:int;
		private var _smallestInt:int;
		private var _largestInt:int;
		private var listStack:Array;
		private var outgoingSymbols:Dictionary;
		
		static private var outgoingVocabulary:Dictionary; 
	    static private var incomingVocabulary:Dictionary; 
	    
	    static private function buildVocabs():void
	    {
	    	outgoingVocabulary = new Dictionary();
	    	incomingVocabulary = new Dictionary();
	    	
	        //# Jelly Data Types
	        outgoingVocabulary["None"] = 1;
	        outgoingVocabulary["class"] = 2;
	        outgoingVocabulary["dereference"] = 3;
	        outgoingVocabulary["reference"] = 4;
	        outgoingVocabulary["dictionary"] = 5;
	        outgoingVocabulary["function"] = 6;
	        outgoingVocabulary["instance"] = 7;
	        outgoingVocabulary["list"] = 8;
	        outgoingVocabulary["module"] = 9;
	        outgoingVocabulary["persistent"] = 10;
	        outgoingVocabulary["tuple"] = 11;
	        outgoingVocabulary["unpersistable"] = 12;
	
	        //# PB Data Types
	        outgoingVocabulary["copy"] = 13;
	        outgoingVocabulary["cache"] = 14;
	        outgoingVocabulary["cached"] = 15;
	        outgoingVocabulary["remote"] = 16;
	        outgoingVocabulary["local"] = 17;
	        outgoingVocabulary["lcache"] = 18;
	
	        //# PB Protocol Messages
	        outgoingVocabulary["version"] = 19;
	        outgoingVocabulary["login"] = 20;
	        outgoingVocabulary["password"] = 21;
	        outgoingVocabulary["challenge"] = 22;
	        outgoingVocabulary["logged_in"] = 23;
	        outgoingVocabulary["not_logged_in"] = 24;
	        outgoingVocabulary["cachemessage"] = 25;
	        outgoingVocabulary["message"] = 26;
	        outgoingVocabulary["answer"] = 27;
	        outgoingVocabulary["error"] = 28;
	        outgoingVocabulary["decref"] = 29;
	        outgoingVocabulary["decache"] = 30;
	        outgoingVocabulary["uncache"] = 31;
	        
	        
	        for (var k:String in outgoingVocabulary)
		    {
		        incomingVocabulary[outgoingVocabulary[k]] = k;
		    }
	        
	    }
	    
		
		
		public static var SIZE_LIMIT:int = 640 * 1024;

		
		//# delimiter characters.
		private static var LIST:uint     = 128;
		private static var INT:uint      = 129;
		private static var STRING:uint   = 130;
		private static var NEG:uint      = 131;
		private static var FLOAT:uint    = 132;
		//# "optional" -- these might be refused by a low-level implementation.
		private static var LONGINT:uint  = 133;
		private static var LONGNEG:uint  = 134;
		//# really optional; this is is part of the 'pb' vocabulary
		private static var VOCAB:uint    = 135;
		
		private static var HIGH_BIT_SET:uint = 128;
		
		static public function int2b128(integer: int, stream: *):void
		{
		    if (integer == 0)
		    {
		        stream(0);
		        return;
		    }
		    if(integer < 0) throw new Error("can only encode positive integers");
		    while (integer)
		    {
		        stream(integer & 0x7f);
		        integer = integer >> 7;
		    }
		        
		}
		
		static public function b1282int(str:String):int
		{
		    
		    var oneHundredAndTwentyEight:uint = 128;
		    var i: int = 0;
		    var place:uint = 0;
		    for(var pos: int = 0;pos < str.length; pos++)
		    {
		        
		        var num:uint = str.charCodeAt(pos);
		        i = i + (num * Math.pow(oneHundredAndTwentyEight , place));
		        place = place + 1;
		    }
		    if (i <= 2147483647)
		    {
		        return int(i);
		    }
		    else
		    {
		        return i;
		    }
		    
		}
		
		public function Banana()
		{
			listStack = [];
			buildVocabs();
			outgoingSymbols = outgoingVocabulary;
			setPrefixLimit(_PREFIX_LIMIT);
			
			this.buffer = new ByteArray();
		}
		
		private function setPrefixLimit(limit:uint):void
		{
	        /*
	        Set the prefix limit for decoding done by this protocol instance.
	        @see: L{setPrefixLimit}
	        */
	        this.prefixLimit = limit;
	        
	        this._smallestInt = - Math.pow(2 , 31);
	        this._largestInt = Math.pow(2 , 31 - 1);
	        
	        // These are too big for Flash
	        //this._smallestLongInt = - Math.pow(2 , ((limit * 7) + 1));
	        //this._largestLongInt = Math.pow(2 , ((limit * 7) - 1));
	        
	        // set them to the smaller limits
	        this._smallestLongInt = this._smallestInt;
	        this._largestLongInt = this._largestInt;
	        
	        
		}
		
		public function connectHandler(event:Event):void
		{
			currentDialect = null;
		}
		
		public function sendEncoded(obj:Object):void
	    {
	        var io:ByteArray = new ByteArray();
	        this._encode(obj, io.writeByte);
	        //StreamTools.prettyPrint(io);
	        this.transport.writeBytes(io);
	        this.transport.flush();
	    }
		
		public function _encode(obj:*, write:Function):void
	    {
	        if (obj is Array)
	        {
	            if (obj.length > SIZE_LIMIT)
	            {
	                throw new BananaError("list/tuple is too long to send " + obj.length);
	            }
	            int2b128(obj.length, write);
	            write(LIST);
	            for (var elem:String in obj)
	            {
	                this._encode(obj[elem], write);
	            }
	        }
	        else if (obj is  int || obj is uint)
	        {
	            if (obj < this._smallestLongInt || obj > this._largestLongInt)
	            {
	                throw new BananaError("int/long is too large to send " + obj);
	            }
	            if (obj < this._smallestInt)
	            {
	                int2b128(-obj, write);
	                write(LONGNEG);
	            }
	            else if (obj < 0)
	            {
	                int2b128(-obj, write);
	                write(NEG);
	            }
	            else if (obj <= this._largestInt)
	            {
	                int2b128(obj, write);
	                write(INT);
	            }
	            else
	            {
	                int2b128(obj, write);
	                write(LONGINT);
	            }
	        }
	        else if (obj is Number)
	        {
	            write(FLOAT);
	            var d:ByteArray = new ByteArray();
	            d.writeDouble(obj);
	            for(var b:int = 0;b < d.length;b++)
	            {
	            	write(d[b]);
	            }
	        }
	        else if (obj is String)
	        {
	            //# TODO: an API for extending banana...
	            if (this.currentDialect == "pb" && (this.outgoingSymbols[obj]!=null))
	            {
	                var symbolID:int = this.outgoingSymbols[obj];
	                int2b128(symbolID, write);
	                write(VOCAB);
	            }
	            else
	            {
	                if (obj.length > SIZE_LIMIT)
	                {
	                    throw new BananaError("string is too long to send " + obj.length);
	                }
	                int2b128(obj.length, write);
	                write(STRING);
	                writeString(obj,write);
	            }
	        }
	        else
	        {
	            throw new BananaError("could not send object: " + obj);
	        }
	    }

	    
	    private function setDialect(dialect:String):void
		{
			currentDialect = dialect;	
		}
	    
	    public function callExpressionReceived(obj:Object):void
	    {
	    	//trace("Banana callExpressionReceived obj - "+obj);
			if(currentDialect)
			{
				expressionReceived(obj);
			}
			else
			{
				for each(var dialect:String in knowDialects)
				{
					for each(var d:String in obj)
					{
						if(d == dialect) 
						{
							setDialect(d);
							trace("d = "+d)
							sendEncoded(d);
							break;
						}
					}
				}
				
			}
	    }
	    
	    public function gotItem(item:Object):void
	    {
	        //trace("item = "+item);
	        
	        var l:Array = this.listStack;
	        
	        if(l.length > 0)
	        {
	            l[l.length-1][1].push(item);
	        }
	        else
	        {
	            this.callExpressionReceived(item);
	        }
	    }
		
		public function expressionReceived(obj:Object):void{}
		
		override public function dataReceived(chunk:*):void
	    {
	        //trace("new chunk length = "+chunk.length);

	        var _chunk: ByteArray = chunk as ByteArray;
	        var _buffer:ByteArray = new ByteArray();
	        if(this.buffer.bytesAvailable) _buffer.writeBytes(this.buffer);
	        _buffer.writeBytes(_chunk);

	        var _listStack:Array = this.listStack;
	        var _gotItem:Function = this.gotItem;
	        
	        StreamTools.prettyPrint(_buffer);
	        
	        _buffer.position = 0;
	        
	        while ( _buffer.length)
	        {
	            if( this.buffer == _buffer) throw new Error("This ain't right: buffers are equal");
	            _buffer.readBytes(this.buffer);

	            var pos:uint = 0;
	            var HIGHBITFLAG:Boolean;
	            var numStr:String = '';
	            for (var p:int = 0;p < _buffer.length; p++)
	            {
	                var ch:int = _buffer[p];
	                //trace("ch = "+ch);
	                
	                if (ch >= HIGH_BIT_SET)
	                {
	                    HIGHBITFLAG = true;
	                    break;
	                }
	                numStr += String.fromCharCode(ch);
	                pos++;
	            }
	            if(!HIGHBITFLAG)
	            {
	                if (pos > this.prefixLimit)
	                {
	                    throw BananaError("Security precaution: more than "+this.prefixLimit+" bytes of prefix");
	                }
	                return;
	            }

	            var numBytes:ByteArray = new ByteArray();
	            numBytes.writeBytes(_buffer,0,pos);
	            var num:String = readByteString(numBytes);
	            var tb:ByteArray = new ByteArray();

	            tb.writeBytes(_buffer,pos,1);
	            
	            var typeByte:int = tb[0];
	            //trace("typeByte = "+typeByte);
	            var rest:ByteArray = new ByteArray();
	            rest.writeBytes(_buffer,pos+1);
	            if (num.length > this.prefixLimit)
	            {
	                throw new BananaError("Security precaution: longer than "+this.prefixLimit+" bytes worth of prefix");
	            }
	            
	            var l:int
	            
	            if (typeByte == LIST)
	            {
	                l = b1282int(num);
	                if (l > SIZE_LIMIT)
	                {
	                    throw new BananaError("Security precaution: List too long.");
	                }
	                _listStack.push([ l, [] ]);
	                _buffer = rest;
	            }
	            else if (typeByte == STRING)
	            {
	                l = b1282int(num);
	                if (l > SIZE_LIMIT)
	                {
	                    throw new BananaError("Security precaution: String too long.");
	                }
	                if (rest.length >= l)
	                {
	                    _buffer = new ByteArray();
	                    _buffer.writeBytes(rest,l);
	                    var itemBytes:ByteArray = new ByteArray();
	                    itemBytes.writeBytes(rest,0,l);
	                    trace("STRING itemBytes");
	                    StreamTools.prettyPrint(itemBytes);
	                    trace();
	                    //_gotItem(readByteString(itemBytes));
	                    //_gotItem(itemBytes.toString());
	                    itemBytes.position = 0;
	                    _gotItem(itemBytes.readMultiByte(itemBytes.length,'iso-8859-1'));
	                }
	                else
	                {	                
	                    return;
	                }
	            }
	            else if (typeByte == INT)
	            {
	                _buffer = rest;
	                l = b1282int(num);
	                _gotItem(l);
	            }
	            else if (typeByte == LONGINT)
	            {
	                _buffer = rest;
	                l = b1282int(num);
	                _gotItem(l);
	            }
	            else if (typeByte == LONGNEG)
	            {
	                _buffer = rest;
	                l = b1282int(num);
	                _gotItem(-l);
	            }
	            else if (typeByte == NEG)
	            {
	                _buffer = rest;
	                l = -b1282int(num);
	                _gotItem(l);
	            }
	            else if (typeByte == VOCAB)
	            {
	                _buffer = rest;
	                l = b1282int(num);
	                _gotItem(incomingVocabulary[l]);
	            }
	            else if (typeByte == FLOAT)
	            {
	                if (rest.length >= 8)
	                {
	                    _buffer = new ByteArray();
	                    _buffer.writeBytes(rest,8);
	                    var floatBytes:ByteArray = new ByteArray();
	                    floatBytes.writeBytes(rest,0,8);
	                    _gotItem(floatBytes.readDouble());
	                }
	                else
	                {
	                    return;
	                }
	            }
	            else
	            {	            
	                throw new NotImplementedError("Invalid Type Byte " +typeByte);
	            }
	            while (_listStack.length > 0 && (_listStack[_listStack.length-1][1].length == _listStack[_listStack.length-1][0]))
	            {
	                var item:Object = _listStack.pop()[1];
	                _gotItem(item);
	            }
	        }
	        this.buffer = new ByteArray();
	        
	    }
	    
	    
	    private function writeString(str:String,stream:Function):void
	    {
	    	var l:uint = 0;
	    	while(l < str.length)
	    	{
	    		stream(str.charCodeAt(l));
	    		l++;
	    	}
	    }
	    
	    private function readByteString(stream:ByteArray):String
	    {
	    	var str:String = '';
	    	for(var i:int = 0;i < stream.length;i++)
	    	{
	    		str += String.fromCharCode(stream[i])
	    	}

	    	return str;
	    }
	}
}